Fisheye views magnify local detail while preserving context, yet projection-aware, scriptable tools for R spatial analysis remain limited. mapycusmaximus introduces a Focus-Glue-Context (FGC) fisheye transform for numeric coordinates and sf geometries. Acting radially around a chosen center, the transform defines a magnified focus, a smooth transitional glue zone, and a fixed exterior. Distances expand or compress via a zoom factor and a power-law squeeze, with an optional angular twist that enhances continuity. The method is projection-conscious: lon/lat inputs are reprojected to suitable CRSs (for example: GDA2020/MGA55), normalized for stable parameter control, and restored afterward. A geometry-safe engine (st_transform_custom) supports all feature types, maintaining ring closure and metadata. The high-level sf_fisheye() integrates with tidyverse, ggplot2, and Shiny, with built-in datasets and tests ensuring reproducibility. By coupling coherent radial warps with tidy, CRS-aware workflows, mapycusmaximus enables spatial exploration that emphasizes local structure without losing global context.
Maps that reveal fine local structure without losing broader context face a persistent challenge: zooming in hides regional patterns, while small-scale views suppress local detail. Traditional solutions such as insets and multi-panel displays break spatial continuity and increase cognitive load (Cockburn et al. 2008).
We introduce mapycusmaximus, an R package which implements a Focus-Glue-Context (FGC) fisheye transformation that continuously warps geographic space. The transformation magnifies a chosen focus region, compresses surrounding areas into a transitional glue zone, and maintains stability in the outer context. The approach operates directly on vector geometry coordinates, preserves topology, and supports a reproducible, pipeline-oriented cartography within the R sf and ggplot2 ecosystem. An optional glue-zone twist (the revolution parameter) can gently rotate features to aid continuity.
The development of focus+context visualization traces back to Furnas (1986)’s degree-of-interest (DOI) function, which introduced a formal method to rank information elements by combining intrinsic importance with distance from the user’s focus. In this model, items with low DOI are de-emphasized or hidden, enabling emphasis on salient regions without losing global structure. Sarkar and Brown (1992, 1994) extended this to geometric distortion, demonstrating smooth magnification transitions for graph visualization. Other approaches to this problem include hyperbolic geometry for hierarchies (Lamping et al. 1995), distortion-view frameworks (Carpendale and Montagnese 2001), and “magic lens” overlays (Bier et al. 1993). A 2008 review from Cockburn et al. (2008)’s covers two decades of research across overview+detail, zooming, and focus+context paradigms.
In cartography, the need for nonlinear magnification emerged independently. Snyder (1987) developed “magnifying-glass” azimuthal projections with variable radial scales. Harrie et al. (2002) created variable-scale functions for mobile devices where user position appears large-scale against small-scale surroundings. An influential contribution came from Yamamoto et al. (2009, 2012), who introduced the Focus+Glue+Context model, in which the intermediate “glue” region absorbs distortion, preventing the excessively warped roads and boundaries that were seen in earlier fisheye maps. This three-zone architecture proved particularly effective for pedestrian navigation and mobile web services.
Despite the Focus+Glue+Context model being introduced more than a decade ago, there is currently no native R solution for implementing these models. The package mapycusmaximus fills this niche by providing a general function to perform these transformations, as well as integrations to work natively within the R spatial ecosystem with sf (Pebesma 2018).
First, we briefly review the current R-based solutions to the detail-versus-context tradeoff. Ths includes multi-panel approaches, hexagon tile maps and cartograms.
Tools like cowplot::ggdraw()(Wilke 2025) can create multi-panel plots, with one panel showing an overview, and another shows zoomed detail (Figure 1).
Figure 1: Overview map with inset showing Greater Melbourne. The main panel displays Victoria, while a secondary inset zooms into metropolitan Melbourne. The separation highlights local detail but requires the reader to mentally integrate focus and context across panels.
These are potentially the most practical approach to the detail-versus-context tradeoff, and may effective for static reports, but require viewers to mentally integrate separate views, and don’t preserve the embedded relationship between focus and context within a single continuous geography. The lack of continuous geography also presents challenges if additional elements such as fill colour are introduced to the plot.
A solution to the lack of continuous geometry is the hexagonal tilemap, such as that implemented by the sugarbag package [REF]. In these maps, a number of tiles are placed in each region of the map, approximately proportional to the size of the population of the region. As a result, large areas with small populations receive relatively few tiles, while small areas with large populations receive many more tiles, meaning that the density of tiles more accurately reflects the number of data points in a region.
Figure 2: Sugarbag hex tile map illustrating thematic spatial abstraction. Original LGA polygons are replaced by uniform hexagons, with fill indicating poplulation and original geography shown faintly beneath. This representation removes area bias but sacrifices precise geographic location.
The challenge with tile maps is that they abstract away precise geography entirely, treating space as a topology-preserving tessellation where “neighbors touch” matters more than accurate boundaries. Tile maps excel at avoiding size bias. However, they abandon continuous spatial relationships. As a result, you cannot identify precise locations, measure distances, or overlay point data meaningfully. In Figure 2, the sugarbag approach was overlaid on top of the original geography, and it is difficult to discern which tiles belong to which geographic area.
A third approach is a cartogram plot. In these plots, areas are intentionally distorted to ensure an area reflects the density of a variable. The most common approach is to distort the shapes such that the area becomes proportional to population (Gastner and Newman 2004). Figure 3 shows a cartogram for Victorian local government areas (LGAs) where color indicates population, and polygons are distorted proportionally to population. Notice how, in comparison to 1, the areas in the south where the bulk of the Victorian population resides take up a majority of the plot.
Figure 3: Population of Victorian LGAs shown as a diffusion cartogram. Colour (and size) indicates population. We can see that the LGAs for greater Melbourne have the highest population, and these are massively exploded.
This approach fundamentally differs from focus+context methods. Cartograms substitute spatial accuracy for data encoding, often severely disrupting shapes and adjacencies. The reader can still extract geographic features and relationships, as the cartogram preserves relative positions and topology, however, the shape and size are distorted.
In contrast, the FGC fisheye transformation preserves relative positions and topology while magnifying a user-selected spatial region rather than a data-driven variable. The use cases are distinct: cartograms address the dominance of a variable in space, whereas fisheye lenses facilitate exploration of local detail within a broader geographic context.
None of these approaches provide continuous geometric magnification within a single, topology-preserving map. The fisheye lens keeps everything in one frame-roads bend smoothly, metropolitan detail enlarges, but you still see how the city sits within its state. It’s a geometric warp rather than a data-driven substitution or panel-based separation. This matters for use cases like: examining hospital networks in Melbourne while maintaining Victorian context, exploring census tracts in a metro core without losing county boundaries, or analyzing transit lines with their regional hinterland visible.
With this landscape established, we now turn to the technical implementation: how does the FGC transformation actually work, and how does this package make it accessible within R’s spatial workflows?
Figure 4: Illustration of Focus-Glue-Context zones in a fisheye transformation. Original grid points are shown alongside their transformed positions, coloured by zone, with arrows indicating displacement. Points expand in the focus, compress smoothly in the glue, and remain fixed in the context.
Consider a point \(P = (x, y)\) in a projected coordinate system. The analyst chooses a center \(C = (c_x, c_y)\) and two radii: \(r_{\text{in}}\) delineating the focus region and \(r_{\text{out}}\) marking the glue boundary. Points inside the focus magnify, points between the radii focus on the center and then compress according to a smooth curve, and points outside remain unchanged. This radial scheme keeps angular coordinates intact, thereby preserving bearings and relative direction.
Let \((r, \theta)\) denote the polar form of point \(P = (x, y)\) relative to center \(C = (c_x, c_y)\). The transformation defines a new radius \(r'\) via a piecewise function:
\[\begin{equation} r' = \begin{cases} \min\left( z \cdot r, r_{\text{in}} \right) & \text{if } r \le r_{\text{in}}, \\ r_{\text{in}} + (r_{\text{out}} - r_{\text{in}}) \cdot h(u; s) & \text{if } r_{\text{in}} < r \le r_{\text{out}}, \\ r & \text{if } r > r_{\text{out}}, \end{cases} \end{equation}\]
where \(z > 1\) is the zoom factor within the focus, \(s \in (0, 1]\) controls glue compression, and \(u = { (r - r_{\text{in}}) }/{ (r_{\text{out}} - r_{\text{in}}) }\) normalizes the glue radius to \([0,1]\). The function \(h(u; s)\) is chosen so that \(h(0; s) = 0\), \(h(1; s) = 1\), and both the first derivatives and the radii match at the boundaries. We adopt a symmetric power curve function,
\[\begin{equation} h(u; s) = \begin{cases} \tfrac{1}{2} \cdot u^{1/s} & \text{if } 0 \le u \le 0.5, \\ 1 - \tfrac{1}{2} \cdot (1 - u)^{1/s} & \text{if } 0.5 < u \le 1, \end{cases} \end{equation}\]
which compresses radii near both boundaries and emphasizes the mid-glue region. An alternative approach which implements outward compression which biases the curve towards \(r_{\text{out}}\) is also implemented with parameter "outward". A demonstration on how original and transformed radius can be seen at the Figure 5. The transform optionally introduces rotation within the glue zone to accentuate the flow from detail to context. Let \(\phi(u)\) denote the angular adjustment. We employ a bell-shaped profile: \(\phi(u) = \rho \cdot 4u(1-u)\), where \(\rho\) is the revolution parameter (in radians). This function peaks at the glue midpoint and vanishes at the boundaries, ensuring continuity.
Figure 5: Radial mapping function of the FGC fisheye. The plot shows original radius r against warped radius r’, with shaded focus, glue, and context regions and a reference identity line. The curve demonstrates expansion in the focus, smooth compression in the glue, and identity mapping outside.
Spatial datasets vary widely in CRS, extent, feature types, and schema. mapycusmaximus follows a disciplined staged workflow where each step is explicit, auditable, and invariant to input type. The architecture separates numeric mapping, spatial orchestration, and geometry reconstruction, allowing the core transform to remain small and testable while sf-specific concerns are isolated in thin wrappers.
The pipeline proceeds: sanitize input -> select working CRS -> normalize -> warp -> denormalize -> restore original CRS. Empty geometries are dropped and sf::st_zm() enforces 2D coordinates.
If the layer is already in a projected CRS, that CRS is used. If it is geographic (lon/lat), the data are transformed to a sensible local projected CRS (for example: UTM inferred from the centroid; for Victoria, GDA2020/MGA55 is typical). Distances are then in metres and parameters behave consistently. The original CRS is restored on return.
Figure 6: Workflow diagram of the normalization and CRS handling pipeline. The flowchart depicts CRS selection, normalization, center resolution, fisheye application, and CRS restoration. This highlights the staged design ensuring projection awareness and parameter stability.
A bounding box defines the normalizing scale. With preserve_aspect = TRUE, a uniform scale \(s = \max(s_x, s_y)\) is applied; otherwise axes scale independently. Center resolution happens before normalization: sf/sfc centers reduce to a centroid then transform to the working CRS; numeric pairs with center_crs are transformed; numeric pairs without CRS are interpreted heuristically; with normalized_center = TRUE, pairs live in \([-1, 1]\) relative to the bbox midpoint. If no center is given, the bbox midpoint is used.
The main function of the mapycusmaximus package is the fisheye_fgc() function, which maps an \(n \times 2\) coordinate matrix to a new \(n \times 2\) matrix using the equations given in Section xxx. The function takes arguments cx and cy, corresponding to the center point, r_in and r_out, denoting the borders of the transformed regions, zoom_factor corresponding to \(z\) in the previous section, a squeeze factor, and potential angular transformation through the revolution argument, which corresponds to \(\rho\).
Figure 7: Basic numeric example of an FGC fisheye transformation. A synthetic grid is shown before and after warping. The example isolates the core radial mapping independent of spatial geometry reconstruction.
The resulting matrix contains the transformed coordinates in the same order as the input coordinates. The zone of each point, and the original and transformed radii of each point from the specified center are returned as invisible attributes named zones, original_radius and new_radius respectively. The choice to return a 2-column matrix was specifically made to ensure ease of operation with sf coordinate construction functions.
x_new y_new
[1,] -1.0 -1
[2,] -0.9 -1
[3,] -0.8 -1
[4,] -0.7 -1
[5,] -0.6 -1
[6,] -0.5 -1
[1] "dim" "dimnames" "zones"
[4] "original_radius" "new_radius"
Numeric stability at zone boundaries is ensured by clamping expansions in the focus so radii do not exceed \(r_{in}\), and using smooth power curves in the glue so derivatives match across boundaries. The radial mapping is vectorized and runs in linear time in the number of vertices as seen in the benchmark 8.
Figure 8: Benchmark performance of fisheye transformations. Log-log plots show runtime scaling for fisheye_fgc() (numeric coordinates) and sf_fisheye() (sf geometries) against input size. Both exhibit near-linear scaling, indicating efficient per-vertex computation.
At the top level is an all-in-one function sf_fisheye(), which presents the user-facing interface while keeping the numeric core untouched. It validates input, selects working CRS, resolves center, constructs normalization closures, and invokes st_transform_custom() to rebuild geometries.
The geometry walker st_transform_custom() applies a user-specified function which transforms coordinates from one coordinate system to another. For each feature, it extracts coordinates via sf::st_coordinates(), yielding a matrix with columns \((x, y, L1, L2, \dots)\) where L1 and L2 index polygon rings and multi-polygon parts. Geometries are split by type:
After transformation, polygon rings are explicitly closed by forcing first and last vertices to equality: \((x_1', y_1') = (x_n', y_n')\). This prevents numerical drift when the warp changes ring curvature. Geometries are rebuilt using sf constructors (st_point(), st_linestring(), st_polygon(), st_multipolygon()), combined into an sfc with original CRS, and spliced back into an sf if appropriate. Attributes are preserved because only the geometry column is replaced.
Table 1 illustrates coordinate transformations across zones for a vertical transect, showing radial expansion in the focus, smooth compression in the glue, and identity mapping in the context.
| x | y | x_new | y_new | zone | r_orig | r_new |
|---|---|---|---|---|---|---|
| -1.0 | -1 | -1.000 | -1.000 | context | 1.414 | 1.414 |
| -0.9 | -1 | -0.900 | -1.000 | context | 1.345 | 1.345 |
| -0.8 | -1 | -0.800 | -1.000 | context | 1.281 | 1.281 |
| -0.7 | -1 | -0.108 | -0.323 | focus | 0.316 | 0.340 |
| -0.6 | -1 | 0.000 | -0.340 | focus | 0.300 | 0.340 |
| -0.5 | -1 | 0.108 | -0.323 | focus | 0.316 | 0.340 |
| -0.4 | -1 | 0.000 | -0.500 | glue | 0.500 | 0.500 |
| -0.3 | -1 | -0.300 | -0.400 | glue | 0.500 | 0.500 |
| -0.2 | -1 | -0.200 | -0.400 | glue | 0.447 | 0.448 |
Utilities in utils.R provide create_test_grid() for diagnostics, classify_zones() for labeling, and plot_fisheye_fgc() for visualization. Dataset documentation in data.R accompanies example layers (vic, vic_fish, conn_fish) used in tests.
For multi-layer maps, the normal process is combine all the layers into a single sf object and apply sf_fisheye(), then split the result later. One minimalist example for this approach is show in the code block below.
# Multi-layer example
bind <- dplyr::bind_rows(
object_1 |> dplyr::mutate(.layer="object_1"),
object_2 |> dplyr::mutate(.layer="object_2"))
bind_w <- sf_fisheye(
bind,
center = melb,
r_in = 0.34,
r_out = 0.55,
zoom = 1.8,
squeeze = 0.35)
object_1_transformed <- bind_w |>
dplyr::filter(.layer == "object_1") |>
dplyr::select(-.layer)
object_2_transformed <- bind_w |>
dplyr::filter(.layer == "object_2") |>
dplyr::select(-.layer)
The test suite mirrors the modular structure, covering boundary behavior, zone labeling, CRS round-trips, ring closure, and performance. Functions follow tidyverse-oriented conventions (snake case parameters, small exported surface). Behaviour is validated by tests; we aim for stability across versions but do not promise guarantees.
The principal user interface is sf_fisheye(), which accepts an sf or sfc object and returns an object of the same top-level class whose geometry has been warped in a projection- aware manner. For clarity, we group arguments into data/CRS handling, center selection, and radial warping, and we make explicit the invariant enforced by the implementation.
Data and CRS. The argument sf_obj supplies the features to be transformed. Before any calculation, empty geometries are removed and Z/M dimensions are dropped using sf::st_zm(), so that downstream computation operates on a strict \(n\times 2\) coordinate matrix. The optional target_crs sets the working projected CRS; if provided, the input is transformed via sf::st_transform() and the original CRS is restored on return. When target_crs = NULL and the input is geographic (lon/lat), a projected working CRS is chosen deterministically from the layer’s centroid: the default value is GDA2020, otherwise a UTM zone is inferred by longitude and hemisphere. This choice ensures the fisheye operates in metric units with bounded distortion across the extent of interest. The preserve_aspect flag governs normalization: with TRUE (default) a uniform scale \(s = \max(s_x, s_y)\) is applied, where \(s_x, s_y\) are bbox half-spans; with FALSE, independent scales are used per axis. Uniform scaling preserves circular symmetry of the focus and glue; per-axis scaling yields an elliptical interpretation that can be useful common for long, narrow extents but should be used deliberately. Degenerate cases \((s_x = 0) or (s_y = 0)\) are handled by substituting a unit scale to avoid division by zero.
Center selection. The lens center may be specified in several forms. The preferred interface is center, which takes precedence over legacy cx, cy. If center is a numeric pair and center_crs is provided (for example: "EPSG:4326"), the point is transformed into the working CRS. If center_crs is omitted, a heuristic interprets pairs that lie within \(\|\text{lon}\|\le 180\), \(\|\text{lat}\|\le 90\) as WGS84 and transforms them accordingly; otherwise the values are assumed to be already in working-CRS map units. Any sf/sfc geometry may be used as center; non-point centers are combined and reduced to a centroid and then transformed to the working CRS, which is often convenient when the focal area is a polygon (for example: a CBD boundary) or a set of points (for example: incident locations). Finally, when the argument {normalized_center = TRUE}, center is interpreted as a pair in \([-1,1]\) relative to the bbox midpoint and the chosen normalization (uniform or per-axis). Normalised centers make parameter sets portable across datasets of different extents and are a natural fit for parameter sweeps in reproducible pipelines. If no center is supplied, the bbox midpoint is used; this default is stable under reprojection.
Radial warping. The radii \(r_{\text{in}}\) and \(r_{\text{out}}\) define the focus and glue boundaries in the normalized coordinate space and must satisfy \(r_{\text{in}} < r_{\text{out}}\). The interpretation of these radii depends on preserve_aspect. With uniform scaling, a circle of radius \(r_{\text{in}}\) in unit space corresponds to a circle of radius \(r_{\text{in}},s\) in map units; with per-axis scaling, the corresponding shape is an axis-aligned ellipse with semi-axes \(r_{\text{in}},s_x\) and \(r_{\text{in}},s_y\). Inside the focus, distances from the center are multiplied by zoom_factor; to prevent overshoot, the implementation clamps (r’) so that points do not cross the \(r_{\text{in}}\) boundary. Across the glue, squeeze_factor in \((0,1]\) controls how strongly intermediate radii compress: smaller values create tighter compression near the boundaries and a more pronounced “shoulder” in the middle of the glue; larger values approach a linear transition. The method selects the family of curves used in the glue. The default "expand" applies a symmetrical power law that expands inward and outward halves of the glue to maintain visual balance around the midpoint; "outward" biases the map towards \(r_{\text{out}}\), keeping the outer boundary steadier and pushing more deformation into the inner portion of the glue. The optional revolution parameter adds a bell-shaped angular twist inside the glue of magnitude \(\rho,4u(1-u)\), where \(u\) is the normalized glue radius. This rotation vanishes at both glue boundaries and peaks at the midpoint, preserving continuity. Positive values rotate counter-clockwise, negative values clockwise; values are specified in radians.
Inter-parameter interactions and invariant. The following constraints and behaviors are enforced: \(r_{\text{out}} > r_{\text{in}} > 0\); zoom_factor \(\ge 1\) (values close to one yield gentle focus); squeeze_factor in \((0,1]\) (\(=1\) approaches linear); and monotonicity of the radial map so that ordering by distance from the center is preserved. The choice of preserve_aspect affects the physical size of radii and thereby the impact of a given parameter set on different datasets; using uniform scaling with a normalized center yields the most portable configurations. Twisting via revolution is confined to the glue; it does not change radii and therefore does not affect the classification of points into zones. Because angles are modified only in the glue, bearings inside the focus and in the context are preserved.
Return value and side effects. The function returns an object of the same top-level class as its input (sf or sfc). For sf inputs, non-geometry columns are preserved verbatim; only the geometry column is replaced. The original CRS is restored before return so that downstream plotting and analysis code does not need to change. On malformed geometries, the implementation emits a warning and returns an empty geometry of the appropriate family to preserve row count and indices. For exploratory diagnostics, the low-level fisheye_fgc() returns a coordinate matrix with attributes "zones", "original_radius", and "new_radius"; these can be used to plot scale curves and verify parameter effects prior to applying the transform to complex geometries.
Although the parameter space is continuous, certain regimes recur in practice and can serve as reliable starting points. We describe these regimes and articulate the trade-offs that motivate each choice. The recommendations assume the default preserve_aspect = TRUE; when per-axis scaling is enabled, translate radii to semi-axes using the bbox half-spans.
Quick start (synthetic grid). Set \(r_{\text{in}}\) to 0.30-0.35 and \(r_{\text{out}}\) to 0.55-0.70. Pair this with zoom_factor between 5 and 10 and squeeze_factor near 0.35 for a balanced focus that still shows context. Stick with method = "expand" and revolution = 0 unless you explicitly need outer rigidity or a twist.
In this simple example, we demo the effect of zoom factor 1.5 and 2 on a synthetic grid to demonstrate how the point movement was effected by the zoom factor.
Figure 9: Effect of zoom factor on fisheye distortion. Two panels compare zoom factors 1.5 and 2 applied to a synthetic grid, with points coloured by zone. Higher zoom increases magnification in the focus while preserving context stability.
However, as demonstrated below, the revolution effect might make the distortion of the linestring object more obvious, comparing to just only the zoom factor effect.
Figure 10: Effect of angular revolution on line geometries. Line paths are shown with revolution set to 0 and pi/8, coloured by zone. Introducing revolution produces a visible rotational flow in the glue region without affecting the focus or context radii.
As we can see, the revolution effect create a vortex or zoom wheel effect into the focus zone, which may be useful in some cases. For manuscripts and dashboards, prefer revolution = 0.
Figure 11: Comparison of glue compression methods. Polygon grids are shown under expand and outward glue modes. The outward method concentrates distortion closer to the focus, while expand distributes compression symmetrically across the glue.
Similarly, start with "expand" and adopt "outward" only when outer stability is an explicit requirement. Always annotate or at least describe the distortion in figure captions so readers do not mistake warped areas for standard projections.
Simple tweaks. If linework kinks, raise squeeze_factor slightly (for example: 0.45). If the focus feels too tight, lower zoom_factor toward 4-6. For reproducible comparisons, keep normalized_center = TRUE and reuse the same radii across runs.
To support interactive exploration of Focus-Glue-Context (FGC) parameters, the package includes a Shiny-based lens explorer. The app allows users to experiment with fisheye settings on a realistic spatial example - Victorian LGAs with a synthetic sampled hospital - Residential Aged Care Facility (RACF) transfer network during COVID 19 - and to observe how points, lines, and polygons respond under a shared geometric warp.
Figure 12: Interactive Focus-Glue-Context Shiny application. Users control lens centre, radii, zoom, and compression via sliders and drag the lens directly on the map, with points, lines, and polygons warped together in real time. The app enables rapid exploration of fisheye parameters before committing to static figures.
The interface is divided into two coordinated panels:
ggplot2 and sf map rendered with the same bounding box and aspect ratio as the fisheye view, enabling fair visual comparison between warped and unwarped representations.Spatial data (LGAs, points, and transfer lines) are converted once into plain coordinate lists and sent to the browser. Subsequent interactions update only lens parameters, ensuring smooth response even during continuous dragging.
Users begin by choosing an Initial lens center (LGA) from the sidebar. Internally, the selected LGA polygon is reduced to a representative point (st_point_on_surface()), which becomes the fisheye center. This mirrors the common scripted workflow of passing a polygon or centroid as the center argument to sf_fisheye().
Once initialized, the center can be moved freely by dragging within the fisheye panel, allowing rapid scanning of different regions without changing parameters.
Two sliders control the spatial extent of distortion:
r_in Sets the size of the magnified region. Smaller values create a tight focal bubble; larger values expand the magnified area.r_out Sets the extent of the transition zone where compression occurs before geometry becomes fixed.Together, these define the Focus-Glue-Context structure used by both fisheye_fgc() and sf_fisheye(). Increasing r_out spreads distortion more gradually; decreasing it concentrates deformation closer to the focus.
Two further sliders adjust distortion strength:
zoom_factor or zoom Controls radial expansion inside the focus. Higher values increase separation of dense features but amplify distortion near the focus boundary.squeeze_factor or squeeze Controls how sharply distances compress within the glue region. Smaller values produce a pronounced ‘shoulder’ near boundaries; larger values yield a smoother transition.Users are encouraged to increase zoom until local structure becomes readable, then adjust squeeze to reduce crowding or sharp curvature near the glue boundary.
The network is intentionally subsampled to maintain interpretability:
n_fac) controls how many hospitals and RACFs are included.All layers (polygons, points, and lines) are warped together using identical parameters, ensuring alignment is preserved under distortion.
When using the app, readers should pay attention to:
These observations help users choose sensible parameter ranges before producing static figures or scripted analyses.
The app deliberately separates parameter exploration from final figure generation. Interactive dragging and sliders provide immediate visual feedback, while the parameter values correspond directly to arguments in sf_fisheye(). Once suitable settings are identified, the same values can be reused verbatim in reproducible code pipelines.
For the current approach, the shiny app only use the default dataset included in the mapycusmaximus package, which are the 2016 Victorian Local Government Areas (LGA) and their boundaries, accompany with the synthetic transfer network between hospital and RACF. In the future stable, we will open up the app for users to upload their own spatial data or piping it directly to the Shiny app from R.
To illustrate the use we use data summarising transfers between residential aged care facilities (RACFs) and Victorian hospitalsin Victoria, over a period that includes the COVID-19 pandemic period. The transfer data is synthetic, for privacy reasons, but locations of the RACFs and hospitals is publicly available, and accurate. To simplify the visualizations we use a sample 10 hospitals and 10 RACFs. Most of the hospitals are located in the Melbourne metropolitan area, and zooming in on this helps to helps to see the transfer volume.
Figure ?? shows the geographical map with the local government areas coloured, locations of RACFs (blue) and hospitals (red) overlaid, and transfers shown as lines. The rural transfers are easy to see, but the volume of transfers happens in the metropolitan area which is too concentrated to see on this map. The fisheye can be used to alleviate this.
Figure 13: Victorian map overlaid by locations of RACFs (blue) and hospitals (red), and lines indicating transfers. Transfers in the mtropolitan region cannot be seen.
We could also include an interactive version of the plot above, thanks to the natively support and integration of ggplotly with sf object.
Figure 14: Fisheye magnification of Greater Melbourne with statewide context. This makes it possible to see the transfers in the metropolition area as well as in the rural locations.
The fisheye clarifies dense metro structure without losing state context. Hospitals and RACFs that previously overlapped separate cleanly; transfer lines remain connected to their endpoints because every layer uses the same center and radii. Outside the glue zone, geometry remains stable, so readers can still place Melbourne within Victoria. To inspect a different hub (for example: Geelong), change center to that polygon; multiple center can be explored by re-running sf_fisheye() with alternative center and comparing panels side by side.
For polygons beyond metro Melbourne, the same lens settings apply; polygon edges bend smoothly but retain adjacency. Because transfer counts are simulated, readers should treat magnitude as illustrative only; spatial relationships (what becomes readable after zoom vs. what remains hidden) follow from the geometry.
In this case, we can see that some point and connection that was inside the context zone stay the same, while the zoom-in effect was applied to the focus zone.
The mapycusmaximus package provides an sf-native implementation of the FGC fisheye that is projection-aware, parameterized in normalized units, and safe across points, lines, and polygons. The package separates radial mapping from geometry orchestration, exposes explicit controls over focus, glue, and context, and preserves attributes and CRS invariant for reproducible pipelines with ggplot2.
Unlike cartograms (thematic distortion), hex/regular tile maps (discrete abstraction), or inset/multi-panel layouts (spatial separation), the FGC lens delivers continuous magnification within a single map while preserving topology and bearings. This reduces cognitive load for readers who must relate local phenomena to their broader geography.
The fisheye introduces non-metric distortion in the focus and glue; therefore, use it for visual exploration and communication, not for metric analysis. Aggressive zoom or squeeze can impair legibility near the glue boundary; conservative defaults and revolution = 0 are recommended for publication maps. When comparing multiple regions, prefer normalized_center = TRUE with fixed radii to ensure visual comparability. At present, exact matching of focus and glue radii across separately transformed layers may require a manual step (the user have to manually merge the two or more layers, perform the fisheye transformation, then separated the transformed layers).
Planned extensions include anisotropic or elliptical profiles, multi-focus blending, first-class raster support via warped grids and resampling, and interactive focus selection for exploratory analysis. We also plan an API for shared normalization and radius locking across layers (for example: a combine_fisheye) so that multiple layers can be warped with identical scale and then returned transformed. Performance improvements via vectorised geometry walkers or GPU acceleration would benefit dense polygonal datasets. Clear figure captions and scale disclaimers remain essential to communicate the presence and intent of distortion.
FGC fisheye transformations offer a concise, CRS-aware way to emphasize local structure without losing geographic context. By starting from a point-wise radial map and integrating carefully with sf for geometry reconstruction, the approach keeps figures continuous and overlays aligned. The examples demonstrate clearer narratives for metropolitan focus while maintaining state or nation-level context.
We used AI tools to assist with code refactoring and drafting portions of the text. All methods, parameter settings, and claims were designed and reviewed by the authors, and we verified outputs with the package’s test suite and example renders.
The github repo for this paper is https://github.com/Alex-Nguyen-VN/paper-mapycusmaximus.
The mapycusmaximus package is available at https://github.com/Alex-Nguyen-VN/mapycusmaximus.
Text and figures are licensed under Creative Commons Attribution CC BY 4.0. The figures that have been reused from other sources don't fall under this license and can be recognized by a note in their caption: "Figure from ...".
For attribution, please cite this work as
Nguyen, et al., "Focus-Glue-Context Fisheye Transformations for Spatial Visualization", The R Journal, 2025
BibTeX citation
@article{paper-mapycusmaximus,
author = {Nguyen, Thanh Cuong and Lydeamore, Michael and Cook, Dianne},
title = {Focus-Glue-Context Fisheye Transformations for Spatial Visualization},
journal = {The R Journal},
year = {2025},
issn = {2073-4859},
pages = {1}
}